/*
    jME Terra package. A Terrain Engine for the Java Monkey Engine.
    Copyright (C) 2005-2006,  Tijl Houtbeckers
    
    **** LICENSE: ****

	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation,
	version 2.1.
	
	This library is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.
	
	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

	**** CONTACT: ****

    I can be reached through the jmonkeyengine.com forums (user "llama"), 
    or the jmonkeyengine.com contact page:
    jmonkeyengine.com/index.php?option=com_contact&task=view&contact_id=5           

*/

/**
 * @author Tijl Houtbeckers
 */


package org.llama.jmex.terra;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * @author tijl
 * HeightMapManager 
 */
public class HeightMapManager {

    private int mapsize;

    public static final String K_MAP = "m";
    public static final String K_CSOURCE = "C";
    public static final String K_MSOURCE = "S";
    public static final String K_COMPRESSED = "c";
    public static final String K_MCOMPRESSOR = "z";

     
    public int getMapSize() {
        return mapsize;
    }

    public HeightMapManager(int mapsize) {
        this.mapsize = mapsize;
    }
   
    private MapStore gcms;

    public void setGlobalCompressedMapStore(MapStore compressedMapSource) {
        this.gcms = compressedMapSource;
    }

    private MapStore gms;

    public void setGlobalMapStore(MapStore mapSource) {
        this.gms = mapSource;
    }
    
    private MapCompressor gmc;
    
    public void setGlobalMapCompressor(MapCompressor mapCompressor) {
    	this.gmc = mapCompressor;
    }
    

    protected Map maps = Collections.synchronizedMap(new HashMap());

    /**
     * generate key for HashMap entry.
     */
    public XYKey gkey(int x, int y, String s) {
        return new XYKey(x, y, s);
    }

//    private int mapcachex = Integer.MAX_VALUE, mapcachey = Integer.MAX_VALUE;
//    private IntBuffer mapcache;

    public ByteBuffer getMapAsByteBuffer(int x, int y) {
    	return (ByteBuffer) maps.get(gkey(x, y, K_MAP));
    }
    
    public IntBuffer getMap(int x, int y) {
    	ByteBuffer buf = (ByteBuffer) maps.get(gkey(x, y, K_MAP));
    	if (buf!=null)
    		return buf.asIntBuffer();
    	return null;
    }
    
    final class MapCache {
    	public int x, y;
    	public IntBuffer buf;
    	MapCache(int x, int y, IntBuffer buf) {
    		this.x = x;
    		this.y = y;
    		this.buf = buf;
    	}
    }
    
    /*
     * Two entries should be enough for good speed unless there are 
     * two different threads fighting, but that should be avoided.
     * The method is thread safe however.
     */
    private MapCache[] mapcache = { new MapCache(Integer.MAX_VALUE, Integer.MAX_VALUE, null), new MapCache(Integer.MAX_VALUE, Integer.MAX_VALUE, null)};
    
    public IntBuffer getMapWithCaching(int x, int y) {
        
    	// unrolled for speed
    	MapCache entry = mapcache[0];
    	if (entry.x == x && entry.y == y && entry.buf!=null) {
    		return entry.buf;
    	}
    	entry = mapcache[1];
    	if (entry.x == x && entry.y == y && entry.buf!=null) {
    		mapcache[1] = mapcache[0];
    		mapcache[0] = entry;
    		return entry.buf;
    	}
    	entry = new MapCache(x, y, ((ByteBuffer) maps.get(gkey(x, y, K_MAP))).asIntBuffer());
    	mapcache[1] = mapcache[0];
    	mapcache[0] = entry;
    	return entry.buf;
    }

    public void addMap(int x, int y, ByteBuffer map) {
        maps.put(gkey(x, y, K_MAP), map);
    }
    
    public Object remove(XYKey xy) {
    	return maps.remove(xy);
    }
    
    public Object get(XYKey xy) {
    	return maps.get(xy);
    }
    
    public void add(XYKey xy, Object o) {
    	maps.put(xy, o);
    }

    public ByteBuffer removeMap(int x, int y) {
        ByteBuffer buf = (ByteBuffer) maps.remove(gkey(x, y, K_MAP));
        return buf;
    }

    public ByteBuffer getCompressedMap(int x, int y) {
        return (ByteBuffer) maps.get(gkey(x, y, K_COMPRESSED));
    }
    
    public void addCompressedMap(int x, int y, ByteBuffer compressedMap) {
        maps.put(gkey(x, y, K_COMPRESSED), compressedMap);
    }

    public ByteBuffer removeCompressedMap(int x, int y) {
        return (ByteBuffer) maps.remove(gkey(x, y, K_COMPRESSED));
    }

    public void addCompressedMapStore(int x, int y, MapStore source) {
        maps.put(gkey(x, y, K_CSOURCE), source);
    }

    public MapStore getCompressedMapStore(int x, int y) {
        MapStore cms = (MapStore) maps.get(gkey(x, y, K_CSOURCE));
        if (cms == null && gcms != null) cms = gcms;
        return cms;
    }
       
    public MapStore removeCompressedMapStore(int x, int y) {
        return (MapStore) maps.remove(gkey(x, y, K_CSOURCE));
    }

    public void addMapStore(int x, int y, MapStore source) {
        maps.put(gkey(x, y, K_MSOURCE), source);
    }
    
    public MapStore getMapStore(int x, int y) {
        MapStore ms = (MapStore) maps.get(gkey(x, y, K_MSOURCE));
        if (ms == null && gms != null) ms = gms;
        return ms;
    }

    public MapStore removeMapStore(int x, int y) {
        return (MapStore) maps.remove(gkey(x, y, K_MSOURCE));
    }
    
    public void addMapCompressor(int x, int y, MapCompressor compressor) {
        maps.put(gkey(x, y, K_MCOMPRESSOR), compressor);
    }
    
    public MapCompressor getMapCompressor(int x, int y) {
        MapCompressor mc = (MapCompressor) maps.get(gkey(x, y, K_MCOMPRESSOR));
        if (mc == null && gmc != null) mc = this.gmc;
        return mc;
    }

    public MapCompressor removeMapCompressor(int x, int y) {
        return (MapCompressor) maps.remove(gkey(x, y, K_MCOMPRESSOR));
    }

    
    public boolean loadMap(int x, int y) throws IOException {
    	MapStore ms = getMapStore(x, y);
    	if (ms == null)
    		return false;
    	return loadMapFromStore(x, y, ms, false, true, false, null);
    }
    
    
    
    /*
     * 
     */
    public boolean loadMapFromStore(int x, int y, MapStore source, boolean compress,
    		boolean keepOriginal, boolean addSource, MapCompressor mapCompressor) throws IOException {
    	if (addSource)
    		addMapStore(x, y, source);
    	
    	ByteBuffer map = source.loadMap(x, y);
    	if (keepOriginal)
    		addMap(x, y, map);
    	
    	if (compress)
    		compressMap(x, y, false, true, map, mapCompressor);
    	
    	return true;    		
    }
    		
    
    public boolean loadCompressedMap(int x, int y) throws IOException {
        MapStore cms = getCompressedMapStore(x, y);
        if (cms == null)
            return false;
        return loadCompressedMapFromStore(x, y, cms, false, true, false, null);
    }
    
    public boolean loadCompressedMapFromStore(int x, int y, MapStore source, boolean decompress,
            boolean keepCompressed, boolean addSource, MapCompressor mapCompressor) throws IOException {
       
        if (addSource) 
            addCompressedMapStore(x, y, source);
        ByteBuffer cmap = source.loadMap(x, y);
        if (keepCompressed) 
            addCompressedMap(x, y, cmap);

        if (decompress) 
            decompressMap(x, y, false, true, cmap, mapCompressor);
            
        return true;
    }
    
    public boolean saveMap(int x, int y) throws IOException {
    	return saveMap(x, y, false, false, false, true, null, null);
    }
    
    public boolean saveMap(int x, int y, boolean decompressIfNeeded, boolean loadCompressedIfNeeded, boolean keepCompressed,
    		boolean keepOriginal, ByteBuffer original, MapCompressor mapCompressor) throws IOException {
    	MapStore source = getMapStore(x, y);
    	if (source == null)
    		return false;
    	
    	if (!saveMapToStore(x, y, source, decompressIfNeeded, keepOriginal, loadCompressedIfNeeded, keepCompressed, original, mapCompressor))
    		return false;
    		
		if (!keepCompressed) 
			removeCompressedMap(x, y);
		
		if (!keepOriginal)
			removeMap(x, y);
    	
    	return true;
    }
    
    public boolean saveMapToStore(int x, int y, MapStore store, boolean decompressIfNeeded, boolean keepOriginal, 
    		boolean loadCompressedIfNeeded, boolean keepCompressed, 
    		ByteBuffer original, MapCompressor mapCompressor) throws IOException {
    	
    	if (original == null)
    		original = getMapAsByteBuffer(x, y);
    	if (original == null) {
    		if (!decompressIfNeeded)
    			return false;
    		if (!decompressMap(x, y, loadCompressedIfNeeded, keepCompressed, null, mapCompressor))
    			return false;
    		original = getMapAsByteBuffer(x, y);
    		if (original == null) {
    			// rah?
    			return false;
    		}
    	}
    	store.saveMap(x, y, original);
    	return true;
    }
    
    public boolean saveCompressedMap(int x, int y) throws IOException {
    	return saveCompressedMap(x, y, false, false, true, true, null, null);
    }
    
    public boolean saveCompressedMap(int x, int y, boolean compressIfNeeded, boolean loadOriginalIfNeeded,  
    		boolean keepOriginal, boolean keepCompressed, ByteBuffer compressed, MapCompressor mapCompressor) throws IOException {
    	MapStore source = getCompressedMapStore(x, y);
        if (source == null) return false;
        
        if (!saveCompressedMapToStore(x, y, source, compressIfNeeded, keepCompressed, loadOriginalIfNeeded, keepOriginal, compressed, mapCompressor))
        	return false;

        if (!keepOriginal) removeMap(x, y);
        if (!keepCompressed) removeCompressedMap(x, y);        
        return true;
    }
    
    public boolean saveCompressedMapToStore(int x, int y, MapStore store, boolean compressIfNeeded, 
    		boolean keepCompressed, boolean loadOriginalIfNeeded, boolean keepOriginal, 
    		ByteBuffer compressed, MapCompressor mapCompressor) throws IOException {
    	
    	if (compressed == null)
    		compressed = getCompressedMap(x, y);
        if (compressed == null) {
            if (!compressIfNeeded) 
            	return false;
            if (!compressMap(x, y, loadOriginalIfNeeded, keepOriginal, null, mapCompressor)) 
            	return false;
            if (keepCompressed)
            	compressed = getCompressedMap(x, y);
            else
            	compressed = removeCompressedMap(x, y);
            
            if (compressed == null) {
                // uhm.. er.. but.. HUH?
                return false;
            }
        }

        store.saveMap(x, y, compressed);       
        return true;
    }
    
    public ByteBuffer createCompressedMap(int x, int y, int size) throws IOException {
    	MapStore store = getCompressedMapStore(x, y);
    	if (store != null)
    		return store.createMap(x, y, size);
    	return ByteBuffer.allocate(size);	
    }
    
    public ByteBuffer createMap(int x, int y) throws IOException {
    	MapStore store = getMapStore(x, y);
    	if (store != null)
    		return store.createMap(x, y, mapsize * mapsize * 4);
    	return ByteBuffer.allocate(mapsize * mapsize * 4);	
    }
    
    public boolean compressMap(int x, int y, boolean loadIfNeeded, boolean keepOriginal) throws IOException {
    	return compressMap(x, y, loadIfNeeded, keepOriginal, null, null);
    }

    public boolean compressMap(int x, int y, boolean loadIfNeeded, boolean keepOriginal, ByteBuffer original, MapCompressor mapCompressor) throws IOException {
    	if (mapCompressor == null)
        	mapCompressor = getMapCompressor(x, y);

    	if (original == null)
        	original = getMapAsByteBuffer(x, y);
        if (original == null) {
        	if (!loadIfNeeded)
        		return false;
        	MapStore store = getMapStore(x, y);
        	if (store != null)
        		return loadMapFromStore(x, y, store, true, keepOriginal, false, mapCompressor);
        	return false;
        }

        ByteBuffer compressed = mapCompressor.compressMap(this, x, y, original);
        addCompressedMap(x, y, compressed);
        if (!keepOriginal) {
            removeMap(x, y);
        }
        return true;
    }
    public boolean decompressMap(int x, int y, boolean loadIfNeeded, boolean keepCompressed) throws IOException {
    	return decompressMap(x,y,loadIfNeeded, keepCompressed, null, null);
    }
    
    public boolean decompressMap(int x, int y, boolean loadIfNeeded, boolean keepCompressed, ByteBuffer compressedMap, MapCompressor mapCompressor) throws IOException {
        if (compressedMap == null) 
        	compressedMap = getCompressedMap(x, y);
        if (mapCompressor == null)
        	mapCompressor = getMapCompressor(x, y);
        if (compressedMap == null) {
            if (!loadIfNeeded) return false;
            MapStore source = getCompressedMapStore(x, y);
            if (source != null) {
                return loadCompressedMapFromStore(x, y, source, true, keepCompressed, false, mapCompressor);
            }
            return false;
        }
        
        ByteBuffer map = mapCompressor.decompressMap(this, x, y, compressedMap);
        addMap(x, y, map);
        if (!keepCompressed) {
            removeCompressedMap(x, y);
        }
        return true;
    }

}